<!DOCTYPE html>

<!--
 All Rights Reserved.
-->

<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by an Apache 2.0 License.
See the COPYING file for details.
-->
<head>
<title>goog.editor.plugins.BasicTextFormatter Tests</title>
<script type="text/javascript" src="../../base.js"></script>
<script type="text/javascript">
  goog.require('goog.array');
  goog.require('goog.dom');
  goog.require('goog.dom.Range');
  goog.require('goog.editor.plugins.BasicTextFormatter');
  goog.require('goog.testing.ExpectedFailures');
  goog.require('goog.testing.PropertyReplacer');
  goog.require('goog.testing.editor.FieldMock');
  goog.require('goog.testing.editor.TestHelper');
  goog.require('goog.testing.jsunit');
  goog.require('goog.userAgent');
</script>
</head>
<body>
<!-- This has to be created here so it is loaded when we need it -->
<iframe id="iframe"></iframe>

<div id="html">
  <ul id="outerUL" type="1">
    <li>foo</li>
    <ul id="innerUL">
      <li>foo</li>
      <li>bar</li>
      <li>baz</li>
    </ul>
    <li>bar</li>
    <li>baz</li>
  </ul>
  ...
  <ul id="outerUL2">
    <li>foo</li>
    <ul>
      <li>foo</li>
      <li>bar</li>
      <li>baz</li>
    </ul>
    <ul>
      <li>foo</li>
      <li>bar</li>
      <li>baz</li>
    </ul>
    <li>bar</li>
    <li>baz</li>
  </ul>

  <ol id="ol">
    <li>foo</li>
    <li>bar</li>
    <li>baz</li>
  </ol>

  <p>before <span id="toQuote">Foo. Bar, baz.</span> after</p>
  <p>before <div id="toQuote2">Foo.<p/>Bar,<p/>baz.</div> after</p>

  <div id="divQuote"><div>lorem</div><div>ipsum</div><div>dolor</div></div>

  <p id="geckolist"><font face="courier">test</font></p>

  <table>
    <tr> <th> head1</th>
      <th id= "outerTh"><span id="emptyTh">head2</span></th> </tr>
    <tr> <td> one </td> <td>two </td> </tr>
    <tr>
      <td> three</td>
      <td id="outerTd"> <span id="emptyTd"><strong>four</strong></span></td>
    </tr>
    <tr id="outerTr"> <td><span id="emptyTr"> five </span></td></tr>
  </table>

  <div id="linkwrapper">Foo<span id="link">Pre<a href="http://www.google.com">Outside Span<span style="font-size:15pt">Inside Span</span></a></span></div>
</div>

<div id="root"></div>

<div id="real-field"></div>

<script type="text/javascript">
  var stubs = new goog.testing.PropertyReplacer();

  var SAVED_HTML = goog.dom.getElement('html').innerHTML;
  var FIELDMOCK;
  var FORMATTER;
  var ROOT = goog.dom.getElement('root');
  var HELPER;
  var OPEN_SUB = goog.userAgent.WEBKIT && !goog.userAgent.isVersion('530') ?
      '<span class="Apple-style-span" style="vertical-align: sub;">' :
      '<sub>';
  var CLOSE_SUB = goog.userAgent.WEBKIT && !goog.userAgent.isVersion('530') ?
                  '</span>' : '</sub>';
  var OPEN_SUPER = goog.userAgent.WEBKIT && !goog.userAgent.isVersion('530') ?
      '<span class="Apple-style-span" style="vertical-align: super;">' :
      '<sup>';
  var CLOSE_SUPER = goog.userAgent.WEBKIT && !goog.userAgent.isVersion('530') ?
                    '</span>' : '</sup>';
  var MOCK_BLOCKQUOTE_STYLE = 'border-left: 1px solid gray;';
  var MOCK_GET_BLOCKQUOTE_STYLES = function() {
    return MOCK_BLOCKQUOTE_STYLE;
  };

  var REAL_FIELD;
  var REAL_PLUGIN;

  var expectedFailures = new goog.testing.ExpectedFailures();

  function setUp() {
    FIELDMOCK = new goog.testing.editor.FieldMock();

    FORMATTER = new goog.editor.plugins.BasicTextFormatter();
    FORMATTER.fieldObject = FIELDMOCK;
  }

  function setUpRealField() {
    REAL_FIELD = new goog.editor.Field('real-field');
    REAL_PLUGIN = new goog.editor.plugins.BasicTextFormatter();
    REAL_FIELD.registerPlugin(REAL_PLUGIN);
    REAL_FIELD.makeEditable();
  }

  function setUpRealFieldIframe() {
    REAL_FIELD = new goog.editor.Field('iframe');
    FORMATTER = new goog.editor.plugins.BasicTextFormatter();
    REAL_FIELD.registerPlugin(FORMATTER);
    REAL_FIELD.makeEditable();
  }

  function selectRealField() {
    goog.dom.Range.createFromNodeContents(REAL_FIELD.getElement()).select();
    REAL_FIELD.dispatchSelectionChangeEvent();
  }

  function tearDown() {
    tearDownFontSizeTests();

    if (REAL_FIELD) {
      REAL_FIELD.makeUneditable();
      REAL_FIELD.dispose();
      REAL_FIELD = null;
    }

    expectedFailures.handleTearDown();
    stubs.reset();

    goog.dom.getElement('html').innerHTML = SAVED_HTML;
  }

  function setUpListAndBlockquoteTests() {
    var htmlDiv = document.getElementById('html');
    HELPER = new goog.testing.editor.TestHelper(htmlDiv);
    HELPER.setUpEditableElement();

    FIELDMOCK.getElement();
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(htmlDiv);
  }

  function tearDownHelper() {
    HELPER.tearDownEditableElement();
    HELPER.dispose();
    HELPER = null;
  }

  function tearDownListAndBlockquoteTests() {
    tearDownHelper();
  }

  function testIEList() {
    if (goog.userAgent.IE) {
      setUpListAndBlockquoteTests();

      FIELDMOCK.queryCommandValue('rtl').$returns(null);

      FIELDMOCK.$replay();
      var ul = goog.dom.getElement('outerUL');
      goog.dom.Range.createFromNodeContents(ul.firstChild.firstChild).select();
      FORMATTER.fixIELists_();
      assertFalse('Unordered list must not have ordered type', ul.type == '1');
      var ol = goog.dom.getElement('ol');
      ol.type = 'disc';
      goog.dom.Range.createFromNodeContents(ol.firstChild.firstChild).select();
      FORMATTER.fixIELists_();
      assertFalse('Ordered list must not have unordered type',
          ol.type == 'disc');
      ol.type = '1';
      goog.dom.Range.createFromNodeContents(ol.firstChild.firstChild).select();
      FORMATTER.fixIELists_();
      assertTrue('Ordered list must retain ordered list type',
          ol.type == '1');
      tearDownListAndBlockquoteTests();
    }
  }

  function testWebKitList() {
    if (goog.userAgent.WEBKIT) {
      setUpListAndBlockquoteTests();

      FIELDMOCK.queryCommandValue('rtl').$returns(null);

      FIELDMOCK.$replay();
      var ul = document.getElementById('outerUL');
      var html = ul.innerHTML;
      goog.dom.Range.createFromNodeContents(ul).select();

      FORMATTER.fixSafariLists_();
      assertEquals('Contents of UL shouldn\'t change',
              html, ul.innerHTML);

      ul = document.getElementById('outerUL2');
      goog.dom.Range.createFromNodeContents(ul).select();

      FORMATTER.fixSafariLists_();
      var childULs = ul.getElementsByTagName('ul');
      assertEquals('UL should have one child UL',
              1, childULs.length);
      tearDownListAndBlockquoteTests();
    }
  }

  function testGeckoListFont() {
    if (goog.userAgent.GECKO) {
      setUpListAndBlockquoteTests();
      FIELDMOCK.queryCommandValue(
          goog.editor.Command.DEFAULT_TAG).$returns('BR').$times(2);

      FIELDMOCK.$replay();
      var p = goog.dom.getElement('geckolist');
      var font = p.firstChild;
      goog.dom.Range.createFromNodeContents(font).select();
      retVal = FORMATTER.beforeInsertListGecko_();
      assertFalse('Workaround shouldn\'t be applied when not needed', retVal);

      font.innerHTML = '';
      goog.dom.Range.createFromNodeContents(font).select();
      var retVal = FORMATTER.beforeInsertListGecko_();
      assertTrue('Workaround should be applied when needed', retVal);
      document.execCommand('insertorderedlist', false, true);
      assertTrue('Font should be Courier',
              /courier/i.test(document.queryCommandValue('fontname')));
      tearDownListAndBlockquoteTests();
    }
  }

  function setUpSubSuperTests() {
    ROOT.innerHTML = '12345';
    HELPER = new goog.testing.editor.TestHelper(ROOT);
    HELPER.setUpEditableElement();
  }

  function tearDownSubSuperTests() {
    tearDownHelper();
  }

  function testSubscriptRemovesSuperscript() {
    setUpSubSuperTests();
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 4); // Selects '234'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUPER+'234'+CLOSE_SUPER+'5');
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUB+'234'+CLOSE_SUB+'5');

    FIELDMOCK.$verify();
    tearDownSubSuperTests();
  }

  function testSuperscriptRemovesSubscript() {
    setUpSubSuperTests();
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 4); // Selects '234'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUB+'234'+CLOSE_SUB+'5');
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUPER+'234'+CLOSE_SUPER+'5');

    FIELDMOCK.$verify();
    tearDownSubSuperTests();
  }

  function testSubscriptRemovesSuperscriptIntersecting() {
    // Tests: 12345 , sup(23) , sub(34) ==> 1+sup(2)+sub(34)+5
    // This is more complex because the sub and sup calls are made on separate
    // fields which intersect each other and queryCommandValue seems to return
    // false if the command is only applied to part of the field.
    setUpSubSuperTests();
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 3); // Selects '23'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUPER+'23'+CLOSE_SUPER+'45');
    HELPER.select('23', 1, '45', 1); // Selects '34'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUPER+'2'+CLOSE_SUPER
                             +OPEN_SUB+'34'+CLOSE_SUB+'5');

    FIELDMOCK.$verify();
    tearDownSubSuperTests();
  }

  function testSuperscriptRemovesSubscriptIntersecting() {
    // Tests: 12345 , sub(23) , sup(34) ==> 1+sub(2)+sup(34)+5
    setUpSubSuperTests();
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 3); // Selects '23'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUBSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUB+'23'+CLOSE_SUB+'45');
    HELPER.select('23', 1, '45', 1); // Selects '34'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.SUPERSCRIPT);
    HELPER.assertHtmlMatches('1'+OPEN_SUB+'2'+CLOSE_SUB
                             +OPEN_SUPER+'34'+CLOSE_SUPER+'5');

    FIELDMOCK.$verify();
    tearDownSubSuperTests();
  }

  function setUpLinkTests(url) {
    stubs.set(window, 'prompt', function() {
      return url;
    });

    ROOT.innerHTML = '12345';
    HELPER = new goog.testing.editor.TestHelper(ROOT);
    HELPER.setUpEditableElement();

    FIELDMOCK.getElement().$anyTimes().$returns(ROOT);

    FIELDMOCK.execCommand(goog.editor.Command.MODAL_LINK_EDITOR,
        goog.testing.mockmatchers.isObject).$returns(undefined);
    FIELDMOCK.setModalMode(true);

    FIELDMOCK.isSelectionEditable().$anyTimes().$returns(true);

    FORMATTER.focusField_ = function() {
      throw 'Field should not be re-focused';
    }
  }

  function tearDownLinkTests() {
    tearDownHelper();
  }

  function testLink() {
    setUpLinkTests('http://www.x.com/');
    FIELDMOCK.$replay();

    HELPER.select('12345', 3);
    FORMATTER.execCommandInternal(goog.editor.Command.LINK);
    HELPER.assertHtmlMatches(
        goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS ?
            '123<a href="http://www.x.com/">http://www.x.com/</a>&nbsp;45' :
            '123<a href="http://www.x.com/">http://www.x.com/</a>45');

    FIELDMOCK.$verify();
    tearDownLinkTests();
  }

  function testSelectedLink() {
    setUpLinkTests('http://www.x.com/');
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 4);
    FORMATTER.execCommandInternal(goog.editor.Command.LINK);
    HELPER.assertHtmlMatches(
        goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS ?
            '1<a href="http://www.x.com/">234</a>&nbsp;5' :
            '1<a href="http://www.x.com/">234</a>5');

    FIELDMOCK.$verify();
    tearDownLinkTests();
  }

  function testCanceledLink() {
    setUpLinkTests(undefined);
    FIELDMOCK.$replay();

    HELPER.select('12345', 1, '12345', 4);
    FORMATTER.execCommandInternal(goog.editor.Command.LINK);
    HELPER.assertHtmlMatches('12345');

    assertEquals('234', FIELDMOCK.getRange().getText());

    FIELDMOCK.$verify();
    tearDownLinkTests();
  }

  function setUpJustifyTests(html) {
    ROOT.innerHTML = html;
    HELPER = new goog.testing.editor.TestHelper(ROOT);
    HELPER.setUpEditableElement(ROOT);

    FIELDMOCK.getElement();
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(ROOT);

    FIELDMOCK.getElement();
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(ROOT);
  }

  function tearDownJustifyTests() {
    tearDownHelper();
  }

  function testJustify() {
    setUpJustifyTests('<div>abc</div><p>def</p><div>ghi</div>');
    FIELDMOCK.$replay();

    HELPER.select('abc', 1, 'def', 1); // Selects 'bcd'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
    HELPER.assertHtmlMatches(
        '<div style="text-align: center">abc</div>' +
        '<p style="text-align: center">def</p>' +
        '<div>ghi</div>');

    FIELDMOCK.$verify();
    tearDownJustifyTests();
  }

  function testJustifyInInline() {
    setUpJustifyTests('<div>a<i>b</i>c</div><div>d</div>');
    FIELDMOCK.$replay();

    HELPER.select('b', 0, 'b', 1); // Selects 'b'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
    HELPER.assertHtmlMatches(
        '<div style="text-align: center">a<i>b</i>c</div><div>d</div>');

    FIELDMOCK.$verify();
    tearDownJustifyTests();
  }

  function testJustifyInBlock() {
    setUpJustifyTests('<div>a<div>b</div>c</div>');
    FIELDMOCK.$replay();

    HELPER.select('b', 0, 'b', 1); // Selects 'h'.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
    HELPER.assertHtmlMatches(
        '<div>a<div style="text-align: center">b</div>c</div>');

    FIELDMOCK.$verify();
    tearDownJustifyTests();
  }

  var isFontSizeTest = false;

  function setUpFontSizeTests() {
    isFontSizeTest = true;
    ROOT.innerHTML = '1<span style="font-size:2px">23</span>4' +
                     '<span style="font-size:5px; white-space:pre">56</span>7';
    HELPER = new goog.testing.editor.TestHelper(ROOT);
    HELPER.setUpEditableElement();
    FIELDMOCK.getElement().$returns(ROOT).$anyTimes();
  }

  function tearDownFontSizeTests() {
    if (isFontSizeTest) {
      tearDownHelper();
      isFontSizeTest = false;
    }
  }

  /**
   * Regression test for {@bug 1286408}. Tests that when you change the font
   * size of a selection, any font size styles that were nested inside are
   * removed.
   */
  function testFontSizeOverridesStyleAttr() {
    setUpFontSizeTests();
    FIELDMOCK.$replay();

    HELPER.select('1', 0, '4', 1); // Selects 1234.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE, 6);
    var span = HELPER.findTextNode('23').parentNode;

    if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR) {
      assertEquals('Old manually set font size should be gone',
          '', span.style.fontSize);
      assertFalse('Style attribute should be gone',
          span.getAttributeNode('style') != null &&
          span.getAttributeNode('style').specified);
    } else {
      assertCSSValueEquals('Old manually set font size should be changed',
          'font-size', 'xx-large', span.style.fontSize);
    }

    FIELDMOCK.$verify();
  }

  /**
   * Make sure the style stripping works when the selection starts and stops in
   * different nodes that both contain font size styles.
   */
  function testFontSizeOverridesStyleAttrMultiNode() {
    setUpFontSizeTests();
    FIELDMOCK.$replay();

    HELPER.select('23', 0, '56', 2); // Selects 23456.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE, 6);
    var span = HELPER.findTextNode('23').parentNode;
    var span2 = HELPER.findTextNode('56').parentNode;

    if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR) {
      assertEquals('Old manually set font size should be gone from first span',
          '', span.style.fontSize);
      assertFalse('Style attribute should be gone from first span',
          span.getAttributeNode('style') != null &&
          span.getAttributeNode('style').specified);
      assertEquals('Old manually set font size should be gone from last span',
          '', span2.style.fontSize);
      assertTrue('Style attribute should not be gone from last span',
          span2.getAttributeNode('style') != null &&
          span2.getAttributeNode('style').specified);
      assertEquals('Whitespace style in last span should have been left',
          'pre', span2.style.whiteSpace);
    } else {
      assertCSSValueEquals(
          'Old manually set font size should be changed in first span',
          'font-size', 'xx-large', span.style.fontSize);
      assertCSSValueEquals(
          'Old manually set font size should be changed in last span',
          'font-size', 'xx-large', span2.style.fontSize);
      // Webkit creates a new span inside the existing span, sets the font
      // size in the inner span, removes it from the outer span, and leaves the
      // pre style in the outer span. Weird but works...
      assertEquals('Whitespace style in last span should have been left',
          'pre', span2.parentNode.style.whiteSpace);
    }

    FIELDMOCK.$verify();
  }

  /**
   * Makes sure the font size style is not removed when only a part of the
   * element with font size style is selected during the font size command.
   */
  function testFontSizeDoesntOverrideStyleAttr() {
    setUpFontSizeTests();
    FIELDMOCK.$replay();

    HELPER.select('23', 1, '4', 1); // Selects 34 (half of span with font size).
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE, 6);
    assertEquals('Old manually set font size should still be there',
        '2px', HELPER.findTextNode('2').parentNode.style.fontSize);

    FIELDMOCK.$verify();
  }

  /**
   * Makes sure the font size style is not removed when only a part of the
   * element with font size style is selected during the font size command, but
   * is removed for another element that is fully selected.
   */
  function testFontSizeDoesntOverrideStyleAttrMultiNode() {
    setUpFontSizeTests();
    FIELDMOCK.$replay();

    HELPER.select('23', 1, '56', 2); // Selects 3456.
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE, 6);
    assertEquals('Old manually set font size should still be in first span',
        '2px', HELPER.findTextNode('2').parentNode.style.fontSize);
    var span2 = HELPER.findTextNode('56').parentNode;
    if (goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR) {
      assertEquals('Old manually set font size should be gone from last span',
          '', span2.style.fontSize);
    } else {
      assertCSSValueEquals(
          'Old manually set font size should be changed in last span',
          'font-size', 'xx-large', span2.style.fontSize);
    }

    FIELDMOCK.$verify();
  }

  /**
   * Helper to make sure the precondition that executing the font size command
   * wraps the content in font tags instead of modifying the style attribute is
   * maintained by the browser even if the selection is already text that is
   * wrapped in a tag with a font size style. We test this with several
   * permutations of how the selection looks: selecting the text in the text
   * node, selecting the whole text node as a unit, or selecting the whole span
   * node as a unit. Sometimes the browser wraps the text node with the font
   * tag, sometimes it wraps the span with the font tag. Either one is ok as
   * long as a font tag is actually being used instead of just modifying the
   * span's style, because our fix for {@bug 1286408} would remove that style.
   * @param {function} doSelect Function to select the "23" text in the test
   *     content.
   */
  function doTestFontSizeStyledSpan(doSelect) {
    // Make sure no new browsers start getting this bad behavior. If they do,
    // this test will unexpectedly pass.
    expectedFailures.expectFailureFor(
        !goog.editor.BrowserFeature.DOESNT_OVERRIDE_FONT_SIZE_IN_STYLE_ATTR);

    try {
    setUpFontSizeTests();
    FIELDMOCK.$replay();

    doSelect();
    FORMATTER.execCommandInternal(
        goog.editor.plugins.BasicTextFormatter.COMMAND.FONT_SIZE, 7);
    var parentNode = HELPER.findTextNode('23').parentNode;
    var grandparentNode = parentNode.parentNode;
    var fontNode = goog.dom.getElementsByTagNameAndClass(goog.dom.TagName.FONT,
                                                         undefined, ROOT)[0];
    var spanNode = goog.dom.getElementsByTagNameAndClass(goog.dom.TagName.SPAN,
                                                         undefined, ROOT)[0];
    assertTrue('A font tag should have been added either outside or inside' +
               ' the existing span',
               parentNode == spanNode && grandparentNode == fontNode ||
               parentNode == fontNode && grandparentNode == spanNode);

    FIELDMOCK.$verify();
    } catch (e) {
      expectedFailures.handleException(e);
    }
  }

  function testFontSizeStyledSpanSelectingText() {
    doTestFontSizeStyledSpan(function() {
      HELPER.select('23', 0, '23', 2);
    });
  }

  function testFontSizeStyledSpanSelectingTextNode() {
    doTestFontSizeStyledSpan(function() {
      var textNode = HELPER.findTextNode('23');
      HELPER.select(textNode.parentNode, 0, textNode.parentNode, 1);
    });
  }

  function testFontSizeStyledSpanSelectingSpanNode() {
    doTestFontSizeStyledSpan(function() {
      var spanNode = HELPER.findTextNode('23').parentNode;
      HELPER.select(spanNode.parentNode, 1, spanNode.parentNode, 2);
    });
  }

  function setUpIframeField(content) {
    var ifr = document.getElementById('iframe');
    var body = ifr.contentWindow.document.body;
    body.innerHTML = content;

    HELPER = new goog.testing.editor.TestHelper(body);
    HELPER.setUpEditableElement();
    FIELDMOCK = new goog.testing.editor.FieldMock(ifr.contentWindow);
    FIELDMOCK.getElement();
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(body);
    FIELDMOCK.queryCommandValue('rtl');
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(null);
    FORMATTER.fieldObject = FIELDMOCK;
  }

  function tearDownIframeField() {
    tearDownHelper();
  }

  function setUpConvertBreaksToDivTests() {
    ROOT.innerHTML = '<p>paragraph</p>one<br id="br1">two<br><br><br>three';
    HELPER = new goog.testing.editor.TestHelper(ROOT);
    HELPER.setUpEditableElement();

    FIELDMOCK.getElement();
    FIELDMOCK.$anyTimes();
    FIELDMOCK.$returns(ROOT);
  }

  function tearDownConvertBreaksToDivTests() {
    tearDownHelper();
  }

  /** @bug 1414941 */
  function testConvertBreaksToDivsKeepsP() {
    if (goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
      return;
    }
    setUpConvertBreaksToDivTests();
    FIELDMOCK.$replay();

    HELPER.select('three', 0);
    FORMATTER.convertBreaksToDivs_();
    assertEquals('There should still be a <p> tag',
                 1, FIELDMOCK.getElement().getElementsByTagName('p').length);
    var html = FIELDMOCK.getElement().innerHTML.toLowerCase();
    assertNotBadBrElements(html);
    assertNotContains('There should not be empty <div> elements',
                      '<div><\/div>', html);

    FIELDMOCK.$verify();
    tearDownConvertBreaksToDivTests();
  }

  /**
  * @bug 1414937
  * @bug 934535
  */
  function testConvertBreaksToDivsDoesntCollapseBR() {
    if (goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
      return;
    }
    setUpConvertBreaksToDivTests();
    FIELDMOCK.$replay();

    HELPER.select('three', 0);
    FORMATTER.convertBreaksToDivs_();
    var html = FIELDMOCK.getElement().innerHTML.toLowerCase();
    assertNotBadBrElements(html);
    assertNotContains('There should not be empty <div> elements',
                      '<div><\/div>', html);
    if (goog.userAgent.IE) {
      // <div><br></div> misbehaves in IE
      assertNotContains(
          '<br> should not be used to prevent <div> from collapsing',
          '<div><br><\/div>', html);
    }

    FIELDMOCK.$verify();
    tearDownConvertBreaksToDivTests();
  }

  function testConvertBreaksToDivsSelection() {
    if (goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
      return;
    }
    setUpConvertBreaksToDivTests();
    FIELDMOCK.$replay();

    HELPER.select('two', 1, 'three', 3);
    var before = FIELDMOCK.getRange().getText().replace(/\s/g, '');
    FORMATTER.convertBreaksToDivs_();
    assertEquals('Selection must not be changed',
                 before, FIELDMOCK.getRange().getText().replace(/\s/g, ''));

    FIELDMOCK.$verify();
    tearDownConvertBreaksToDivTests();
  }

  /** @bug 1414937 */
  function testConvertBreaksToDivsInsertList() {
    setUpConvertBreaksToDivTests();
    FIELDMOCK.$replay();

    HELPER.select('three', 0);
    FORMATTER.execCommandInternal('insertorderedlist');
    assertTrue('Ordered list must be inserted',
               FIELDMOCK.getEditableDomHelper().getDocument().queryCommandState(
                   'insertorderedlist'));
    tearDownConvertBreaksToDivTests();
  }

  /**
   * Regression test for {@bug 1939883}, where if a br has an id, it causes
   * the convert br code to throw a js error. This goes a step further and
   * ensures that the id is preserved in the resulting div element.
   */
  function testConvertBreaksToDivsKeepsId() {
    if (goog.editor.BrowserFeature.CAN_LISTIFY_BR) {
      return;
    }
    setUpConvertBreaksToDivTests();
    FIELDMOCK.$replay();

    HELPER.select('one', 0, 'two', 0);
    FORMATTER.convertBreaksToDivs_();
    var html = FIELDMOCK.getElement().innerHTML.toLowerCase();
    assertNotBadBrElements(html);
    var idBr = document.getElementById('br1');
    assertNotNull('There should still be a tag with id="br1"', idBr);
    assertEquals('The tag with id="br1" should be a <div> now',
                 goog.dom.TagName.DIV,
                 idBr.tagName);
    assertNull('There should not be any tag with id="temp_br"',
               document.getElementById('temp_br'));

    FIELDMOCK.$verify();
    tearDownConvertBreaksToDivTests();
  }

  /**
   * @bug 2420054
   */
  var JUSTIFICATION_COMMANDS = [
      goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT,
      goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT,
      goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER,
      goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL
  ];
  function doTestIsJustification(command) {
    setUpRealField();
    REAL_FIELD.setHtml(false, 'foo');
    selectRealField();
    REAL_FIELD.execCommand(command);

    for (var i = 0; i < JUSTIFICATION_COMMANDS.length; i++) {
      if (JUSTIFICATION_COMMANDS[i] == command) {
        assertTrue('queryCommandValue(' +JUSTIFICATION_COMMANDS[i] +
                   ') should be true after execCommand(' + command + ')',
            REAL_FIELD.queryCommandValue(JUSTIFICATION_COMMANDS[i]));
      } else {
        assertFalse('queryCommandValue(' +JUSTIFICATION_COMMANDS[i] +
                   ') should be false after execCommand(' + command + ')',
            REAL_FIELD.queryCommandValue(JUSTIFICATION_COMMANDS[i]));
      }
    }
  }
  function testIsJustificationLeft() {
    doTestIsJustification(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT);
  }
  function testIsJustificationRight() {
    doTestIsJustification(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT);
  }
  function testIsJustificationCenter() {
    doTestIsJustification(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
  }
  function testIsJustificationFull() {
    doTestIsJustification(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL);
  }

  /**
   * Regression test for {@bug 1414813}, where all 3 justification buttons are
   * considered "on" when you first tab into the editable field. In this
   * situation, when lorem ipsum is the only node in the editable field iframe
   * body, mockField.getRange() returns an empty range.
   */
  function testIsJustificationEmptySelection() {
    var mockField = new goog.testing.LooseMock(goog.editor.Field);

    mockField.getRange();
    mockField.$anyTimes();
    mockField.$returns(null);
    mockField.getPluginByClassId('Bidi');
    mockField.$anyTimes();
    mockField.$returns(null);
    FORMATTER.fieldObject = mockField;

    mockField.$replay();

    assertFalse('Empty selection should not be justified',
                FORMATTER.isJustification_(
                    goog.editor.plugins.BasicTextFormatter.
                    COMMAND.JUSTIFY_CENTER));
    assertFalse('Empty selection should not be justified',
                FORMATTER.isJustification_(
                    goog.editor.plugins.BasicTextFormatter.
                    COMMAND.JUSTIFY_FULL));
    assertFalse('Empty selection should not be justified',
                FORMATTER.isJustification_(
                    goog.editor.plugins.BasicTextFormatter.
                    COMMAND.JUSTIFY_RIGHT));
    assertFalse('Empty selection should not be justified',
                FORMATTER.isJustification_(
                    goog.editor.plugins.BasicTextFormatter.
                    COMMAND.JUSTIFY_LEFT));

    mockField.$verify();
  }

  function testIsJustificationSimple1() {
    setUpRealField();
    REAL_FIELD.setHtml(false,'<div align="right">foo</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertTrue(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationSimple2() {
    setUpRealField();
    REAL_FIELD.setHtml(false,'<div style="text-align: right;">foo</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertTrue(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationComplete1() {
    setUpRealField();
    REAL_FIELD.setHtml(false,
        '<div align="left">a</div><div align="right">b</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationComplete2() {
    setUpRealField();
    REAL_FIELD.setHtml(false,
        '<div align="left">a</div><div align="left">b</div>');
    selectRealField();

    assertTrue(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationComplete3() {
    setUpRealField();
    REAL_FIELD.setHtml(false,
        '<div align="right">a</div><div align="right">b</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertTrue(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationComplete4() {
    setUpRealField();
    REAL_FIELD.setHtml(false,
        '<div align="right"><div align="left">a</div></div>' +
        '<div align="right">b</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertTrue(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  function testIsJustificationComplete5() {
    setUpRealField();
    REAL_FIELD.setHtml(false,
        '<div align="right">a</div>b' +
        '<div align="right">c</div>');
    selectRealField();

    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT));
    assertFalse(REAL_FIELD.queryCommandValue(
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT));
  }

  /** @bug 2472589 */
  function doTestIsJustificationPInDiv(useCss, align, command) {
    setUpRealField();
    var html = '<div ' +
               (useCss ? 'style="text-align:' : 'align="') + align +
               '"><p>foo</p></div>';
    REAL_FIELD.setHtml(false, html);
    selectRealField();
    assertTrue(
        'P inside ' + align + ' aligned' + (useCss ? ' (using CSS)' : '') +
            ' DIV should be considered ' + align + ' aligned',
        REAL_FIELD.queryCommandValue(command));
  }
  function testIsJustificationPInDivLeft() {
    doTestIsJustificationPInDiv(false, 'left',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT);
  }
  function testIsJustificationPInDivRight() {
    doTestIsJustificationPInDiv(false, 'right',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT);
  }
  function testIsJustificationPInDivCenter() {
    doTestIsJustificationPInDiv(false, 'center',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
  }
  function testIsJustificationPInDivJustify() {
    doTestIsJustificationPInDiv(false, 'justify',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL);
  }
  function testIsJustificationPInDivLeftCss() {
    doTestIsJustificationPInDiv(true, 'left',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_LEFT);
  }
  function testIsJustificationPInDivRightCss() {
    doTestIsJustificationPInDiv(true, 'right',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_RIGHT);
  }
  function testIsJustificationPInDivCenterCss() {
    doTestIsJustificationPInDiv(true, 'center',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_CENTER);
  }
  function testIsJustificationPInDivJustifyCss() {
    doTestIsJustificationPInDiv(true, 'justify',
        goog.editor.plugins.BasicTextFormatter.COMMAND.JUSTIFY_FULL);
  }


  function testPrepareContent() {
    setUpRealField();
    assertPreparedContents("\n", "\n");

    if (goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES) {
      assertPreparedContents("&nbsp;<script>alert('hi')<" + "/script>",
          "<script>alert('hi')<" + "/script>");
    } else {
      assertNotPreparedContents("<script>alert('hi')<" + "/script>");
    }

    if (goog.editor.BrowserFeature.CONVERT_TO_B_AND_I_TAGS) {
      assertPreparedContents("<b id='foo'>hi</b>",
          "<strong id='foo'>hi</strong>");
      assertPreparedContents("<i>hi</i>", "<em>hi</em>");
      assertNotPreparedContents("<embed/>");
    } else {
      assertNotPreparedContents("<em>hi</em>");
      assertNotPreparedContents("<strong id='foo'>hi</strong>");
    }
  }

  function testScrubImagesRemovesCustomAttributes() {
    var fieldElem = goog.dom.getElement('real-field');
    fieldElem.innerHTML = '';
    var attrs = {'src': 'http://www.google.com/foo.jpg',
                 'tabIndex': '0',
                 'tabIndexSet': '0'};
    attrs[goog.HASH_CODE_PROPERTY_] = '0';
    goog.dom.appendChild(fieldElem,
        goog.dom.createDom('img', attrs));

    setUpRealField();

    var html = REAL_FIELD.getCleanContents();
    assert('Images must not have forbidden attributes: ' + html,
        -1 == html.indexOf('tabIndex') && -1 == html.indexOf('closure'));
    assert('Image URLs must not be made relative by default: ' + html,
        -1 != html.indexOf('/foo.jpg'));
  }

  function testGeckoSelectionChange() {
    if (!goog.userAgent.GECKO || !goog.userAgent.isVersion('1.9')) {
      return;
    }

    setUpRealFieldIframe();
    // Use native selection for this test because goog.dom.Range will
    // change selections of <br>
    var ifr = document.getElementById('iframe');
    ifr.contentWindow.document.body.innerHTML = 'hello<br id="br1"><br id="br2">';
    var body = ifr.contentWindow.document.body;
    var range = REAL_FIELD.getRange();
    var browserRange = range.getBrowserRangeObject();
    browserRange.setStart(body, 2);
    browserRange.setEnd(body, 2);
    FORMATTER.applyExecCommandGeckoFixes_('formatblock');
    var updatedRange = REAL_FIELD.getRange().getBrowserRangeObject();
    assertEquals('Range should have been updated to deep range',
        'br2', updatedRange.startContainer.id);
    assertEquals('Range should have been updated to deep range',
        0, updatedRange.startOffset);
  }

  function testIEExecCommandFixes() {
    if (!goog.userAgent.IE) {
      return;
    }

    setUpRealField();
    REAL_FIELD.setHtml(false, '<blockquote>hi</blockquote>');
    goog.dom.Range.createFromNodeContents(REAL_FIELD.getElement()).select();

    var nodes = REAL_PLUGIN.applyExecCommandIEFixes_('insertOrderedList');
    assertHTMLEquals(
        '<blockquote>hi <div style="height:0px"></div></blockquote>',
        REAL_FIELD.getCleanContents());
  }

  /**
   * Assert that the prepared contents matches the expected.
   */
  function assertPreparedContents(expected, original) {
    assertEquals(expected,
        REAL_FIELD.reduceOp_(
            goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, original));
  }

  /**
   * Assert that sanitization doesn't affect the given text.
   */
  function assertNotPreparedContents(text) {
    assertPreparedContents(text, text);
  }

  /**
   * Assert that only BR elements expected to persist after convertBreaksToDivs_
   * are in the HTML.
   */
  function assertNotBadBrElements(html) {
    if (goog.userAgent.IE) {
      assertNotContains('There should not be <br> elements', '<br', html);
    } else {
      assertFalse('There should not be <br> elements, except ones to prevent ' +
                  '<div>s from collapsing' + html,
                  /(?!<div>)<br>(?!<\/div>)/.test(html))
    }
  }

</script>
</body>
</html>
